package core;

import java.io.IOException;
import java.net.ServerSocket;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;

import util.Debug;
import messages.Notification;
import messages.StoredMessage;
import net.Connection;
import net.Daemon;

/**
 * Spawn a servent with a daemon port
 * 
 * @author Emil Wall
 */
public class Servent extends Thread {

	private static final int SLEEP_MS = 200;
	private Hashtable<String, Connection> connectors;
	private Hashtable<String, Connection> connections;
	private Hashtable<Integer, StoredMessage> myMessages;
	private LinkedList<String> connectQueue;
	private LinkedList<String> disconnectQueue;
	private Daemon daemon;
	private final int myPort;
	private String name;
	private Main master;
	private boolean running = true;
	private int messageID = 0;
	
	public String lastMsgFrom = null;
	
	/**
	 * Spawns a new Servent Node
	 * @param listen_port to listen on
	 * @param init_port to connect to
	 * @param name of the servent
	 * @param master JFrame containing servent
	 */
	public Servent(int listen_port, String name, Main master) {
		this.name = name;
		myPort = listen_port;
		this.master = master;
		connectors = new Hashtable<String, Connection>();
		connections = new Hashtable<String, Connection>();
		myMessages = new Hashtable<Integer, StoredMessage>();
		connectQueue = new LinkedList<String>();
		disconnectQueue = new LinkedList<String>();
	}
	
	/**
	 * Allows other threads to discontinue execution of this thread
	 * @param run false to stop execution in orderly manner.
	 */
	public void setRunning(boolean run) {
		synchronized(this) {
			running = run;
		}
		
		if(!running) {
			// Clean up daemon and connector threads
			daemon.cleanup();
			synchronized(this) {
				Enumeration<Connection> iter = connectors.elements();
				while(iter.hasMoreElements()) {
					Connection temp = iter.nextElement();
					temp.shouldRun(false);
				}
				iter = connections.elements();
				while(iter.hasMoreElements()) {
					Connection temp = iter.nextElement();
					temp.shouldRun(false);
				}
			}
			connectors.clear();
			connections.clear();
		}
	}
	
	/**
	 * Get method for running status
	 * @return true if Servent is (supposed to be) still running, else false
	 */
	public synchronized boolean getRunning() {
		return running;
	}
	
	/**
	 * run method. Spawns daemon and init connector.
	 */
	public void run() {
		master.writeToLog(name, "SERVENT thread started...");
		
		// Daemon
		try {
			daemon = new Daemon(myPort, name, master, this);
		} catch (IOException e) {
			if(Debug.on) {
				master.writeToLog(name, "Error: Daemon port already in use.");
				master.writeToLog(name, "SERVENT thread terminating...");
			}
			if(Debug.verbose) {
				e.printStackTrace();
			}
			master.stop();
			return;
		}
		daemon.start();
		
		// Periodically sleep until terminated
		while(getRunning()) {
			try {
				String host = null;
				synchronized(this) {
					host = connectQueue.poll();
				}
				if(host != null) {
					String[] split = host.split(":");
					int port = Integer.parseInt(split[1]);
					master.addConnection(connectTo(split[0], port));
				}
				synchronized(this) {
					host = disconnectQueue.poll();
				}
				if(host != null) {
					String[] split = host.split(":");
					int port = Integer.parseInt(split[1]);
					disconnectFrom(split[0], port);
				}
				removeInactiveConnections();
				Thread.sleep(SLEEP_MS);
			} catch (InterruptedException e) {
				if(Debug.verbose) {
					e.printStackTrace();
				}
			}
		}
		Collection<Connection> conns = connections.values();
		for(Connection c : conns) {
			c.shouldRun(false);
		}
		connections.clear();
		master.writeToLog(name, "SERVENT thread terminating...");
	}
	
	/**
	 * Spawns a new connection to another node.
	 * @param ip String representation of ip adress to connect to, 
	 * or null if localhost
	 * @param port remote Daemon port
	 * @return 
	 */
	public String connectTo(String ip, int port) {
		if(getRunning()) {
			Connection conn = new Connection(ip, port, name, master);
			synchronized(connectors) {
				String key = conn.getHost();
				connectors.put(key, conn);
				master.writeToLog(name, "put: "+key);
			}
			conn.start();
			return conn.getHost();
		} else if(Debug.on) {
			master.writeToLog(name, "Servent not running. " +
					"Ignoring connection request.");
		}
		return null;
	}
	
	/**
	 * Terminates an existing connection to another node.
	 * @param ip String representation of ip adress to disconnect from, 
	 * or null if localhost
	 * @param port remote Daemon port
	 */
	public void disconnectFrom(String ip, int port) {
		if(ip == null) {
			ip = "localhost";
		}
		if(getRunning()) {
			Connection conn = null;
			synchronized(this) {
				String key = ip+":"+port;
				master.writeToLog(name, "remove: "+key);
				conn = connectors.remove(key);
			}
			conn.shouldRun(false);
		} else if(Debug.on) {
			master.writeToLog(name, "Servent not running. " +
					"Ignoring disconnect request.");
		}
	}
	
	public String[] getConnections() {
		int length = connectors.size();
		String[] conns = new String[length+connections.size()];
		Enumeration<String> e = connectors.keys();
		for(int i=0; i<length; i++) {
			conns[i] = e.nextElement();
		}
		for(int i=length; i<conns.length; i++) {
			conns[i] = connections.get(i-length).getAddress();
		}
		return conns;
	}
	
	private synchronized void removeInactiveConnections() {
		Enumeration<String> conns = connections.keys();
		while(conns.hasMoreElements()) {
			String key = conns.nextElement();
			Connection c = connections.get(key);
			if(!c.shouldRun()) {
				connections.remove(key);
			}
		}
	}
	
	/** @param obj ignored (set to null) */
	public void notify(Object obj) {
		StoredMessage sm = myMessages.get(messageID-1);
		String name = sm.ownerID;
		Notification ad = new Notification(name, messageID);
		notify(ad);
	}
	
	private void notify(Notification ad) {
		synchronized(this) {
			Enumeration<String> e = connectors.keys();
			for(int i=0; i<connectors.size(); i++) {
				Connection temp = connectors.get(e.nextElement());
				temp.notify(ad);
				master.writeToLog(name, "notifying "+temp.getHost());
			}
		}
		synchronized(this) {
			for(int i=0; i<connections.size(); i++) {
				Connection temp = connections.get(i);
				temp.notify(ad);
				master.writeToLog(name, "notifying "+temp.getHost());
			}
		}
	}
	
	public void postMessage(String message) {
		String name = "localhost:"+myPort;
		StoredMessage sm = new StoredMessage(name, messageID, message);
		myMessages.put(messageID, sm);
		// TODO save message in file
		Notification ad = new Notification(name, messageID);
		notify(ad);
		messageID++;
	}
	
	public void search(String name) {
		master.writeToLog(name, "search not yet implemented.");
	}
	
	public synchronized int addConnection() throws IOException {
		//Add a new socket from any available port
		ServerSocket temp = new ServerSocket(0);
		Connection c = new Connection(temp, master);
		String host = c.getAddress();
		connections.put(host, c);
		connections.get(host).start();
		master.addConnection(host);
		master.writeToLog(name, host);
		return temp.getLocalPort();
	}
	
	public synchronized boolean unique(String host) {
		boolean isConnector = false;
		boolean isConnection = false;
		synchronized(connectors) {
			isConnector = connectors.containsKey(host);
		}
		synchronized(connections) {
			isConnection = connections.containsKey(host);
		}
		return !isConnector && !isConnection;
	}
	
	public synchronized void disconnect(String host) {
		disconnectQueue.add(host);
	}
	
	public synchronized void connect(String host) {
		connectQueue.add(host);
	}

}
